package org.hibernate.cfg;

import org.hibernate.AnnotationException;
import org.hibernate.MappingException;
import org.hibernate.annotations.ManyToAny;
import org.hibernate.annotations.Target;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.jaxb.Origin;
import org.hibernate.boot.jaxb.SourceType;
import org.hibernate.cfg.annotations.HCANNHelper;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.StringHelper;
import org.jboss.logging.Logger;

import javax.persistence.*;
import java.util.*;

/***
 * 重写jpa生成字段映射顺序
 */
public class PropertyContainer {
//
//    static {
//        System.setProperty("jboss.i18n.generate-proxies", "true");
//    }

        private static final CoreMessageLogger LOG = Logger.getMessageLogger(CoreMessageLogger.class, org.hibernate.cfg.PropertyContainer.class.getName());

        /**
         * The class for which this container is created.
         */
        private final XClass xClass;
        private final XClass entityAtStake;

        /**
         * Holds the AccessType indicated for use at the class/container-level for cases where persistent attribute
         * did not specify.
         */
        private final AccessType classLevelAccessType;

        private final LinkedHashMap<String, XProperty> persistentAttributeMap;

        PropertyContainer(XClass clazz, XClass entityAtStake, AccessType defaultClassLevelAccessType) {
            this.xClass = clazz;
            this.entityAtStake = entityAtStake;

            if ( defaultClassLevelAccessType == AccessType.DEFAULT ) {
                // this is effectively what the old code did when AccessType.DEFAULT was passed in
                // to getProperties(AccessType) from AnnotationBinder and InheritanceState
                defaultClassLevelAccessType = AccessType.PROPERTY;
            }

            AccessType localClassLevelAccessType = determineLocalClassDefinedAccessStrategy();
            assert localClassLevelAccessType != null;

            this.classLevelAccessType = localClassLevelAccessType != AccessType.DEFAULT
                    ? localClassLevelAccessType
                    : defaultClassLevelAccessType;
            assert classLevelAccessType == AccessType.FIELD || classLevelAccessType == AccessType.PROPERTY;

            this.persistentAttributeMap = new LinkedHashMap<String, XProperty>();

            final List<XProperty> fields = xClass.getDeclaredProperties( AccessType.FIELD.getType() );
            final List<XProperty> getters = xClass.getDeclaredProperties( AccessType.PROPERTY.getType() );

            preFilter( fields, getters );

            final Map<String,XProperty> persistentAttributesFromGetters = new HashMap<String, XProperty>();

            collectPersistentAttributesUsingLocalAccessType(
                    persistentAttributeMap,
                    persistentAttributesFromGetters,
                    fields,
                    getters
            );
            collectPersistentAttributesUsingClassLevelAccessType(
                    persistentAttributeMap,
                    persistentAttributesFromGetters,
                    fields,
                    getters
            );
        }

        private void preFilter(List<XProperty> fields, List<XProperty> getters) {
            Iterator<XProperty> propertyIterator = fields.iterator();
            while ( propertyIterator.hasNext() ) {
                final XProperty property = propertyIterator.next();
                if ( mustBeSkipped( property ) ) {
                    propertyIterator.remove();
                }
            }

            propertyIterator = getters.iterator();
            while ( propertyIterator.hasNext() ) {
                final XProperty property = propertyIterator.next();
                if ( mustBeSkipped( property ) ) {
                    propertyIterator.remove();
                }
            }
        }

        private void collectPersistentAttributesUsingLocalAccessType(
                LinkedHashMap<String, XProperty> persistentAttributeMap,
                Map<String,XProperty> persistentAttributesFromGetters,
                List<XProperty> fields,
                List<XProperty> getters) {

            // Check fields...
            Iterator<XProperty> propertyIterator = fields.iterator();
            while ( propertyIterator.hasNext() ) {
                final XProperty xProperty = propertyIterator.next();
                final Access localAccessAnnotation = xProperty.getAnnotation( Access.class );
                if ( localAccessAnnotation == null
                        || localAccessAnnotation.value() != javax.persistence.AccessType.FIELD ) {
                    continue;
                }

                propertyIterator.remove();
                persistentAttributeMap.put( xProperty.getName(), xProperty );
            }

            // Check getters...
            propertyIterator = getters.iterator();
            while ( propertyIterator.hasNext() ) {
                final XProperty xProperty = propertyIterator.next();
                final Access localAccessAnnotation = xProperty.getAnnotation( Access.class );
                if ( localAccessAnnotation == null
                        || localAccessAnnotation.value() != javax.persistence.AccessType.PROPERTY ) {
                    continue;
                }

                propertyIterator.remove();

                final String name = xProperty.getName();

                // HHH-10242 detect registration of the same property getter twice - eg boolean isId() + UUID getId()
                final XProperty previous = persistentAttributesFromGetters.get( name );
                if ( previous != null ) {
                    throw new org.hibernate.boot.MappingException(
                            LOG.ambiguousPropertyMethods(
                                    xClass.getName(),
                                    HCANNHelper.annotatedElementSignature( previous ),
                                    HCANNHelper.annotatedElementSignature( xProperty )
                            ),
                            new Origin( SourceType.ANNOTATION, xClass.getName() )
                    );
                }

                persistentAttributeMap.put( name, xProperty );
                persistentAttributesFromGetters.put( name, xProperty );
            }
        }

        private void collectPersistentAttributesUsingClassLevelAccessType(
                LinkedHashMap<String, XProperty> persistentAttributeMap,
                Map<String,XProperty> persistentAttributesFromGetters,
                List<XProperty> fields,
                List<XProperty> getters) {
            if ( classLevelAccessType == AccessType.FIELD ) {
                for ( XProperty field : fields ) {
                    if ( persistentAttributeMap.containsKey( field.getName() ) ) {
                        continue;
                    }

                    persistentAttributeMap.put( field.getName(), field );
                }
            }
            else {
                for ( XProperty getter : getters ) {
                    final String name = getter.getName();

                    // HHH-10242 detect registration of the same property getter twice - eg boolean isId() + UUID getId()
                    final XProperty previous = persistentAttributesFromGetters.get( name );
                    if ( previous != null ) {
                        throw new org.hibernate.boot.MappingException(
                                LOG.ambiguousPropertyMethods(
                                        xClass.getName(),
                                        HCANNHelper.annotatedElementSignature( previous ),
                                        HCANNHelper.annotatedElementSignature( getter )
                                ),
                                new Origin( SourceType.ANNOTATION, xClass.getName() )
                        );
                    }

                    if ( persistentAttributeMap.containsKey( name ) ) {
                        continue;
                    }

                    persistentAttributeMap.put( getter.getName(), getter );
                    persistentAttributesFromGetters.put( name, getter );
                }
            }
        }

        public XClass getEntityAtStake() {
            return entityAtStake;
        }

        public XClass getDeclaringClass() {
            return xClass;
        }

        public AccessType getClassLevelAccessType() {
            return classLevelAccessType;
        }

        public Collection<XProperty> getProperties() {
            assertTypesAreResolvable();
            return Collections.unmodifiableCollection( persistentAttributeMap.values() );
        }

        private void assertTypesAreResolvable() {
            for ( XProperty xProperty : persistentAttributeMap.values() ) {
                if ( !xProperty.isTypeResolved() && !discoverTypeWithoutReflection( xProperty ) ) {
                    String msg = "Property " + StringHelper.qualify( xClass.getName(), xProperty.getName() ) +
                            " has an unbound type and no explicit target entity. Resolve this Generic usage issue" +
                            " or set an explicit target attribute (eg @OneToMany(target=) or use an explicit @Type";
                    throw new AnnotationException( msg );
                }
            }
        }
//
//	private void considerExplicitFieldAndPropertyAccess() {
//		for ( XProperty property : fieldAccessMap.values() ) {
//			Access access = property.getAnnotation( Access.class );
//			if ( access == null ) {
//				continue;
//			}
//
//			// see "2.3.2 Explicit Access Type" of JPA 2 spec
//			// the access type for this property is explicitly set to AccessType.FIELD, hence we have to
//			// use field access for this property even if the default access type for the class is AccessType.PROPERTY
//			AccessType accessType = AccessType.getAccessStrategy( access.value() );
//            if (accessType == AccessType.FIELD) {
//				propertyAccessMap.put(property.getName(), property);
//			}
//            else {
//				LOG.debug( "Placing @Access(AccessType.FIELD) on a field does not have any effect." );
//			}
//		}
//
//		for ( XProperty property : propertyAccessMap.values() ) {
//			Access access = property.getAnnotation( Access.class );
//			if ( access == null ) {
//				continue;
//			}
//
//			AccessType accessType = AccessType.getAccessStrategy( access.value() );
//
//			// see "2.3.2 Explicit Access Type" of JPA 2 spec
//			// the access type for this property is explicitly set to AccessType.PROPERTY, hence we have to
//			// return use method access even if the default class access type is AccessType.FIELD
//            if (accessType == AccessType.PROPERTY) {
//				fieldAccessMap.put(property.getName(), property);
//			}
//            else {
//				LOG.debug( "Placing @Access(AccessType.PROPERTY) on a field does not have any effect." );
//			}
//		}
//	}

//	/**
//	 * Retrieves all properties from the {@code xClass} with the specified access type. This method does not take
//	 * any jpa access rules/annotations into account yet.
//	 *
//	 * @param access The access type - {@code AccessType.FIELD}  or {@code AccessType.Property}
//	 *
//	 * @return A maps of the properties with the given access type keyed against their property name
//	 */
//	private LinkedHashMap<String, XProperty> initProperties(AccessType access) {
//		if ( !( AccessType.PROPERTY.equals( access ) || AccessType.FIELD.equals( access ) ) ) {
//			throw new IllegalArgumentException( "Access type has to be AccessType.FIELD or AccessType.Property" );
//		}
//
//		//order so that property are used in the same order when binding native query
//		LinkedHashMap<String, XProperty> propertiesMap = new LinkedHashMap<String, XProperty>();
//		List<XProperty> properties = xClass.getDeclaredProperties( access.getType() );
//		for ( XProperty property : properties ) {
//			if ( mustBeSkipped( property ) ) {
//				continue;
//			}
//			// HHH-10242 detect registration of the same property twice eg boolean isId() + UUID getId()
//			XProperty oldProperty = propertiesMap.get( property.getName() );
//			if ( oldProperty != null ) {
//				throw new org.hibernate.boot.MappingException(
//						LOG.ambiguousPropertyMethods(
//								xClass.getName(),
//								HCANNHelper.annotatedElementSignature( oldProperty ),
//								HCANNHelper.annotatedElementSignature( property )
//						),
//						new Origin( SourceType.ANNOTATION, xClass.getName() )
//				);
//			}
//
//			propertiesMap.put( property.getName(), property );
//		}
//		return propertiesMap;
//	}

        private AccessType determineLocalClassDefinedAccessStrategy() {
            AccessType classDefinedAccessType;

            AccessType hibernateDefinedAccessType = AccessType.DEFAULT;
            AccessType jpaDefinedAccessType = AccessType.DEFAULT;

            org.hibernate.annotations.AccessType accessType = xClass.getAnnotation( org.hibernate.annotations.AccessType.class );
            if ( accessType != null ) {
                hibernateDefinedAccessType = AccessType.getAccessStrategy( accessType.value() );
            }

            Access access = xClass.getAnnotation( Access.class );
            if ( access != null ) {
                jpaDefinedAccessType = AccessType.getAccessStrategy( access.value() );
            }

            if ( hibernateDefinedAccessType != AccessType.DEFAULT
                    && jpaDefinedAccessType != AccessType.DEFAULT
                    && hibernateDefinedAccessType != jpaDefinedAccessType ) {
                throw new MappingException(
                        "@AccessType and @Access specified with contradicting values. Use of @Access only is recommended. "
                );
            }

            if ( hibernateDefinedAccessType != AccessType.DEFAULT ) {
                classDefinedAccessType = hibernateDefinedAccessType;
            }
            else {
                classDefinedAccessType = jpaDefinedAccessType;
            }
            return classDefinedAccessType;
        }

        private static boolean discoverTypeWithoutReflection(XProperty p) {
            if ( p.isAnnotationPresent( OneToOne.class ) && !p.getAnnotation( OneToOne.class )
                    .targetEntity()
                    .equals( void.class ) ) {
                return true;
            }
            else if ( p.isAnnotationPresent( OneToMany.class ) && !p.getAnnotation( OneToMany.class )
                    .targetEntity()
                    .equals( void.class ) ) {
                return true;
            }
            else if ( p.isAnnotationPresent( ManyToOne.class ) && !p.getAnnotation( ManyToOne.class )
                    .targetEntity()
                    .equals( void.class ) ) {
                return true;
            }
            else if ( p.isAnnotationPresent( ManyToMany.class ) && !p.getAnnotation( ManyToMany.class )
                    .targetEntity()
                    .equals( void.class ) ) {
                return true;
            }
            else if ( p.isAnnotationPresent( org.hibernate.annotations.Any.class ) ) {
                return true;
            }
            else if ( p.isAnnotationPresent( ManyToAny.class ) ) {
                if ( !p.isCollection() && !p.isArray() ) {
                    throw new AnnotationException( "@ManyToAny used on a non collection non array property: " + p.getName() );
                }
                return true;
            }
            else if ( p.isAnnotationPresent( Type.class ) ) {
                return true;
            }
            else if ( p.isAnnotationPresent( Target.class ) ) {
                return true;
            }
            return false;
        }

        private static boolean mustBeSkipped(XProperty property) {
            //TODO make those hardcoded tests more portable (through the bytecode provider?)
            return property.isAnnotationPresent( Transient.class )
                    || "net.sf.cglib.transform.impl.InterceptFieldCallback".equals( property.getType().getName() )
                    || "org.hibernate.bytecode.internal.javassist.FieldHandler".equals( property.getType().getName() );
        }

}
